ESLint 설정하기

  • 2025-08-17
  • 저자: AK

그동안 ESLint 설정하느라 보낸 시간을 다 합치면 적어도 10시간, 최대 100시간은 될텐데 아직도 ESLint 설정 파일이 어떻게 작동하는지 정확히 모른다. 어떤 일에 100시간을 썼는데 쌓인 게 없으면 뭔가 잘못하고 있다는 뜻이다. 2시간 정도 진득하게 공부를 해봤다.

(2025년 8월 17일 v9.33.0 “Flat Config” 기준)

공부 환경 갖추기

뭘 고쳤을 때 어떻게 바뀌는지에 대한 빠른 피드백을 받는 게 중요하다.

설정 파일을 디버깅하는 방법에 대한 공식 문서를 읽어보니 npx eslint --inspect-config라는 명령이 있다. 실행하면 현재 적용된 설정을 일목요연하게 보여주는 웹 페이지가 열린다. 설정 파일을 수정하면 페이지의 내용이 자동으로 갱신된다.

빈 설정에서 시작하기

기본 설정이 뭔지 알아보자.

import { defineConfig } from "eslint/config"

export default defineConfig([])

이렇게 했을 때의 기본 설정은 다음과 같다.

[
  {
    files: ["**/*.js", "**/*.mjs"],
    ignores: [".git/", "**/node_modules/"],
    languageOptions: {
      sourceType: 'module',
      ecmaVersion: 'latest'
    },
    linterOptions: {
      reportUnusedDisableDirectives: 1
    }
  },
  {
    files: ["**/*.cjs"],
    languageOptions: {
      sourceType: 'commonjs',
      ecmaVersion: 'latest'
    }
  }
]

모듈 JS(*.js*.mjs)와 커먼 JS(*.cjs)의 소스 타입을 별도로 지정한 게 눈에 띈다. 아직은 아무런 규칙도 없다.

tseslint.config

typescript-eslint 패키지를 쓰려면 defineConfig() 대신에 tseslint.config()를 쓰라고 안내하고 있다. 시키는대로 교체를 해도 설정에는 아무 변화가 없다. 찾아보니 기능엔 차이가 없고 defineConfig()로 인해 발생하는 타입 문제를 잡아주기 위해 필요하다고 한다.

import tseslint from "typescript-eslint"

// eslint.defineConfig() has a types incompatibility issue
export default tseslint.config([])

여전히 아무런 규칙도 없는 상황.

Global ignores

소스코드 전체에 걸쳐 추가로 무시하고 싶은 파일들이 있다면 아무런 다른 키는 없고 오로지 ignores만 있는 설정을 추가해야 한다고 하는데, name은 추가해도 괜찮았다. (s4는 현재 작업 중인 프로젝트 이름이다. 이런 식으로 ”/” 기호를 써주면 ESLint Config Inspector가 예쁘게 렌더링을 해준다.)

import tseslint from "typescript-eslint"

// eslint.defineConfig() has a types incompatibility issue
export default tseslint.config([
  {
    name: "s4/global ignores",
    ignores: [".cursor/", ".github/", "dist/", "coverage/"],
    // do not add anything else here to make `ignores` apply globally
  },
])

name 이외의 다른 키(예: rules)를 추가하면 ignores는 더이상 글로벌로 작동하지 않는다. 실수하기 딱 좋다. 그래서 의도를 더 명확히 드러내고 실수 방지를 도와주기 위한 함수를 제공한다.

import { globalIgnores } from "eslint/config";

export default tseslint.config([
  globalIgnores([".cursor/", ".github/", "dist/", "coverage/"]),
])

ESLint recommended

이제 규칙을 추가해보자.

@eslint/js에는 allrecommended 설정이 담겨 있다. recommended에 뭐가 있는지 console.log()로 찍어보면 아래와 같다.

{
  rules: {
    'constructor-super': 'error',
    'for-direction': 'error',
    // ...중략...
    'use-isnan': 'error',
    'valid-typeof': 'error'
  }
}

rules만 정의하고 있는 자바스크립트 객체다. 이걸 아래와 같이 ESLint 설정에 추가하면 무슨 일이 벌어지나?

import js from "@eslint/js"

export default tseslint.config([
  // ...중략...
  js.configs.recommended,
])

인스펙터에서는 61개의 규칙이 “모든 파일”에 적용된다고 나온다. 왜 그럴까? 문서를 읽어보니 files를 생략하면 다른 설정에서 지정한 모든 files에 적용된다고 한다. ESLint 기본 설정이 *.js, *.mjs, *.cjs를 명시하고 있으니 이 새 패턴의 파일들에 적용된다고 보면 되겠다.

한편, 문서에서 권장하는 방식은 아래와 같다.

[
  // ...중략...
  { files: ["**/*.js"], plugins: { js }, extends: ["js/recommended"] },
]

다만 extends에 문자열을 쓰는 방식은 eslint-typescript-pluginconfig()에서는 사용할 수 없으므로 아래와 같이 쓰면 된다.

[
  // ...중략...
  { files: ["**/*.js"], extends: [js.configs.recommended] },
]

사실 js.config.recommended{rules: […]} 형태의 객체이므로 아래와 같이 Spread 연산자 를 써도 된다.

[
  // ...중략...
  { ...js.configs.recommended, files: ["**/*.js"] },
]

다만 규칙을 추가하거나 덮어쓰고 싶을 때 이런 식으로 번거로워질 수 있다.

{
  ...js.configs.recommended,
  rules: {
    ...js.configs.recommended.rules,
    "max-params": ["error", { "max": 5 }],
  }
}

extends를 쓰면 아래처럼이 깔끔하게 된다.

{
  files: ["**/*.js"],
  extends: [js.configs.recommended],
  rules: {
    "max-params": ["error", { "max": 5 }],
  }
}

eslint-typescript-plugin: strictTypeChecked

eslint-typescript-plugin의 “Typed Linting” 기능을 써서 타입 시스템을 바짝 조여보자. recommended 대신 strictTypeCheck 설정을 쓰면 된다.

다만 이걸 쓰려면 아래처럼 languageOptions를 추가로 지정해줘야 한다.

{
  extends: [ts.configs.strictTypeChecked],
  languageOptions: { parserOptions: { projectService: true } },
  rules: {
    '@typescript-eslint/restrict-template-expressions': 'off',
    "@typescript-eslint/switch-exhaustiveness-check": "error",
  }
}

최종본

최종본. AI 에이전트가 만드는 코드의 품질을 강제할 목적으로 좀 과하게 조였다.

다만, 린터만으로는 한계가 있고 다른 수단들이 더 필요하다. 예(jscpd: 중복 코드 감지; dependency-cruise: 모듈 간 의존 구조 강제; knip: 안쓰는 코드 감지. 자세한 내용은 에이전트 기반 코딩 실험 3 참고)

import js from "@eslint/js"
import { globalIgnores } from "eslint/config"
import importPlugin from "eslint-plugin-import"
import jsdoc from "eslint-plugin-jsdoc"
import sonarjs from "eslint-plugin-sonarjs"
import ts from "typescript-eslint"

// eslint.defineConfig() has a types incompatibility issue
export default ts.config([
  // global ignores
  globalIgnores([".cursor/", ".github/", "dist/", "coverage/", ".dependency-cruiser.cjs", "eslint.config.js"]),

  // check for typescript files
  { name: "s4/ts", files: ["src/**/*.ts"] },

  // js/recommended with custom rules
  {
    name: "s4/js-recomm-mod",
    extends: [js.configs.recommended],
    rules: {
      "no-undef": "off",
      "max-params": ["error", { max: 5 }],
      "max-statements": ["error", { max: 15 }],
    },
  },

  // jsdoc
  {
    name: "s4/jsdoc-recomm-mod",
    extends: [jsdoc.configs["flat/recommended-error"]],
    rules: {
      "jsdoc/require-jsdoc": ["error", { publicOnly: true }],
      "jsdoc/require-param-type": "off",
      "jsdoc/require-returns-type": "off",
      "jsdoc/require-returns-check": "error",
    },
  },

  // import
  {
    name: "s4/import-recomm-mod",
    extends: [importPlugin.flatConfigs.recommended],
    ignores: ["eslint.config.js"],
    rules: {
      "import/max-dependencies": ["error", { max: 8, ignoreTypeImports: false }],
    },
  },

  // typescript-eslint
  {
    name: "s4/ts-strict-type-checked-mod",
    extends: [ts.configs.strictTypeChecked],
    languageOptions: { parserOptions: { projectService: true } },
    rules: {
      "@typescript-eslint/restrict-template-expressions": "off",
      "@typescript-eslint/switch-exhaustiveness-check": "error",
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-magic-numbers": [
        "error",
        {
          ignore: [-2, -1, 0, 1, 2, 10, 16, 24, 32, 42, 60, 100, 255, 256, 512, 1024],
          ignoreEnums: true,
          ignoreNumericLiteralTypes: true,
          ignoreReadonlyClassProperties: true,
          ignoreTypeIndexes: true,
        },
      ],
    },
  },

  // sonarjs
  {
    name: "s4/sonarjs-recomm-mod",
    extends: [sonarjs.configs.recommended],
    rules: {
      "sonarjs/todo-tag": "off",
      "sonarjs/pseudo-random": "off",
      "sonarjs/no-os-command-from-path": "off",
      "sonarjs/prefer-regexp-exec": "off",
      "sonarjs/cognitive-complexity": ["error", 6],
      "sonarjs/max-lines": ["error", { maximum: 200 }],
      "sonarjs/elseif-without-else": "error",
      "sonarjs/no-collapsible-if": "error",
      "sonarjs/no-inconsistent-returns": "error",
      "sonarjs/slow-regex": "off",
      "no-useless-escape": "off",
      "no-magic-numbers": "off",
    },
  },

  // tests (overrides previous rules)
  {
    name: "s4/test",
    files: ["src/**/*.test.ts"],
    rules: {
      "max-statements": ["error", { max: 20 }],
      "sonarjs/cognitive-complexity": ["error", 3],
      "sonarjs/max-lines": ["error", { maximum: 300 }],
    },
  },
])

몇 가지 재미난(?) 설정들:

  • 테스트 케이스는 좀 길어져도 괜찮지만 인지복잡도는 프로덕션 코드에 비해 더 낮아야 함
  • 전체 파일에 대해서는 max-lines를 제야하고, 개별 함수에 대해서는 max-statements를 제약

2025 © ak